package rwt.server.railroad;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;

import org.slf4j.*;

import rwt.server.RailServer;
import rwt.server.core.*;

/**
 * An external railroad program that communicates over TCP sockets. Examples include the Controller program,
 * the LocoBuffer Server, and the Railroad simulator (RailroadBig.exe). Typically, the controller plus either
 * the LocoBuffer Server or the simulator are run, so there will be two instances of this class.
 */
public class RailroadSystem implements ITcpSocketListener, IConnection
{
	private static final Logger logger = LoggerFactory.getLogger(RailroadSystem.class);
	
	private static final long STARTUP_TIMEOUT_MILLIS = 35_000;
	private static final long PING_INTERVAL_MILLIS = 1_000;
	
	public static RailroadSystem create(File exeFile, String args, int port, String name) throws FileNotFoundException
	{
		return new RailroadSystem(exeFile, args, port, name);
	}
	
	private File exeFile;
	private String args;
	private int port;
	private String name;
	
	private TcpConnection connection;
	
	private RailroadSystem(File exeFile, String args, int port, String name) throws FileNotFoundException
	{
		this.name = name;
		this.exeFile = exeFile;
		if (!exeFile.isFile())
			throw new FileNotFoundException("Executable file '" + exeFile + "' not found.");
		
		this.args = args;
		this.port = port;
		this.connection = new TcpConnection(TcpConnection.LOCALHOST, this.port, this);
	}
	
	public void start() throws Exception
	{
		if (isProcessRunning(exeFile.getName()))
		{
			if (!isConnected())
				connect(STARTUP_TIMEOUT_MILLIS);
		}
		else
		{
			Runtime r = Runtime.getRuntime();
			r.exec("cmd.exe /c start " + exeFile.getName() + " " + args, null, exeFile.getParentFile());
			connect(STARTUP_TIMEOUT_MILLIS);
		}
	}
	
	public void stop()
	{
	}
	
	public void resetConnection()
	{
		connection.reset();
	}
	
	public boolean isConnected()
	{
		return connection.isConnected();
	}
	
	public void tryConnect()
	{
		if (isProcessRunning(exeFile.getName()))
		{
			if (!isConnected())
			{
				try
				{
					connect(STARTUP_TIMEOUT_MILLIS);
					logger.info("Connection opened to '" + exeFile.getName() + "'.");
				}
				catch (InterruptedException | TimeoutException e)
				{
					logger.error("Railroad application '" + exeFile.getName() + "' is running, but server" + 
						" could not open TCP connection.", e);
				}
			}
		}
	}
	
	private void connect(long timeoutMillis) throws InterruptedException, TimeoutException
	{
		long startTime = System.currentTimeMillis();
		while (!connection.connect())
		{
			Thread.sleep(PING_INTERVAL_MILLIS);
			long elapsed = System.currentTimeMillis() - startTime;
			if (elapsed > timeoutMillis)
				throw new TimeoutException("Timed out while waiting for system start-up.");
		}
	}
	
	@Override
	public void onTcpMessage(byte[] data)
	{
		int curPos = 0;
		//Find all messages in data
		while (curPos < data.length)
		{
			int msglen = data[curPos] + 256 * data[curPos + 1]; //convert 2 byte length into int
			byte[] dataNoLengthMarker = Arrays.copyOfRange(data, curPos + 2, curPos + 2 + msglen);
			curPos = curPos + 2 + msglen;
			logger.info("Data received: " + Arrays.toString(dataNoLengthMarker));
			
			BaseMessage msg = null;
			try
			{
				msg = RailServer.getMessageAdaptor().bytesToMessage(dataNoLengthMarker);
			}
			catch (Exception e)
			{
				logger.error("Error converted bytes to message.", e);
			}
			if (msg == null)
				return;
			
			MessageType type = MessageType.fromId(msg.id);
			logger.info("Message " + type.getMessageClass().getName() + " received");
			MessageHandler<BaseMessage> handler = type.getHandler();
			if (handler != null)
			{
				try
				{
					handler.handle(msg, this);
				}
				catch (Exception e)
				{
					logger.warn("", e);
				}
			}
		}
	}
	
	@Override
	public void write(BaseMessage message)
	{
		try
		{
			/*
			 * TODO: in TcpConnection, prepend two bytes describing packet length
			 */
			byte[] bytes = RailServer.getMessageAdaptor().messageToBytes(message);
			logger.info("To " + name + ": " + Arrays.toString(bytes));
			connection.send(bytes);
		}
		catch (Exception e)
		{
			logger.warn("Error converting message to bytes.", e);
		}
	}
	
	@Override
	public void close()
	{
		connection.close();
	}
	
	private static boolean isProcessRunning(final String processName)
	{
		// Maximum length of time (in milliseconds) to wait for 'tasklist' exec to finish (to avoid hanging)
		final long TIMEOUT = 500;
		
		// Exec 'tasklist' command on a separate thread to avoid hanging the entire web server
		Callable<Boolean> task = new Callable<Boolean>()
		{
			@Override
			public Boolean call() throws Exception
			{
				try
				{
					String command = "tasklist /FI \"IMAGENAME eq " + processName + "\"";
					Process p = Runtime.getRuntime().exec(command);
					
					String output = "";
					try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())))
					{
						List<String> lines = new ArrayList<>(4);
						String line = "";
						StringBuilder sb = new StringBuilder();
						while ((line = reader.readLine()) != null)
						{
							sb.append(line.trim());
						}
						output = sb.toString();
						p.waitFor();
					}
					
					// Use regex on output to determine if process is running (it'll give the PID, too)
					Pattern regex = Pattern.compile(processName + "\\s+(\\d{1,6})\\s+", Pattern.CASE_INSENSITIVE);
					Matcher m = regex.matcher(output);
					if (m.find())
					{
						logger.info("  - Process '" + processName + "' is running with PID " + m.group(1));
						return true;
					}
					else
					{
						logger.info("  - Process '" + processName + "' is not running.");
						return false;
					}
				}
				catch (Exception e)
				{
					logger.warn("", e);
				}
				
				return false;
			}
		};
		
		FutureTask<Boolean> ft = new FutureTask<>(task);
		ft.run();
		
		try
		{
			Boolean result = ft.get(TIMEOUT, TimeUnit.MILLISECONDS);
			if (result == null)
			{
				logger.warn("isProcessRunning returned null when attempting to ascertain running " +
						"state of process '" + processName + "'. Process assumed to be not running.");
				return false;
			}
			else
			{
				return result;
			}
		}
		catch (TimeoutException e)
		{
			logger.warn("isProcessRunning exceeded timeout of " + TIMEOUT + " millisecones when " +
					"attempting to ascertain running state of process '" + processName + "'.");
		}
		catch (InterruptedException | ExecutionException e)
		{
			logger.warn("isProcessRunning encountered an unexpected error when attempting to  " +
					"ascertain running state of process '" + processName + "'.", e);
		}
		
		return false; // Error or timeout: assume process to not be running
	}
}
